/*  GStruct: data structure helpers for Java
 *  Copyright (C) 2011 Giulio Franco
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package gstruct.cli;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Manages a list of arguments and orchestrates the parsing.
 *  
 * @author Giulio Franco <giulio_franco@msn.com>
 *
 * @param <ResultType>	Type of the result produced
 */
public class CLIArglist<ResultType> {
	/**
	 * Enumerator for common parse errors
	 * @author Giulio Franco <giulio_franco@msn.com>
	 */
	public static enum Errors {
		UNRECOGNIZED_ARGUMENT("cliarg.error.unrecognized_argument"),
		MISSING_REQUIRED("cliarg.error.missing_required");
		
		public final String errorFormat;
		private Errors(String errorFormat) {
			this.errorFormat = errorFormat;
		}
	}
	
	private final HashSet<CLIArgument<? super ResultType>> requiredArguments;
	private final Map<String, CLIArgument<? super ResultType>> arguments;
	private CLIArgument<? super ResultType> defaultHandler = null;
	
	/**
	 * Create a CLIArglist
	 * @param args	Arguments
	 */
	public CLIArglist(CLIArgument<? super ResultType>... args) {
		int load = Math.max(args.length * 2, 16);
		requiredArguments = new HashSet<>(load);
		arguments = new HashMap<>(load);
		for (CLIArgument<? super ResultType> arg : args) {
			if (arguments.containsKey(arg.matcher())) {
				throw new IllegalArgumentException(String.format(
						"Argument %s has the same matcher (%s) as argument %s",
						arg, arg.matcher(), arguments.get(arg.matcher())));
			} else {
				arguments.put(arg.matcher(), arg);
				if (arg.isRequired()) {
					requiredArguments.add(arg);
				}
			}
		}
	}
	
	/**
	 * Gets the set of required arguments
	 * @return Required arguments
	 */
	public Set<CLIArgument<? super ResultType>> getRequiredArguments() {
		return requiredArguments;
	}
	
	/**
	 * Gets the argument map
	 * @return Map from argument names to arguments
	 */
	public Map<String, CLIArgument<? super ResultType>> getArguments() {
		return arguments;
	}

	/**
	 * Performs an argument parse on the given string arguments
	 * @param result	Fresh result, that will be filled during the parse
	 * @param args		String tokenized arguments
	 * @return			Parse result
	 */
	public ParseResult<ResultType> parse(ResultType result, String... args) {
		return parse(result, Arrays.asList(args));
	}
	
	/**
	 * Performs an argument parse on the given string arguments
	 * @param result	Fresh result, that will be filled during the parse
	 * @param args		String tokenized arguments
	 * @return			Parse result
	 */
	public ParseResult<ResultType> parse(
			ResultType result, List<String> args) {
		return parse(result, args.iterator());
	}
	
	/**
	 * Gets the default handler for unmatched arguments
	 * @return	Default argument handler
	 */
	public CLIArgument<? super ResultType> getDefaultHandler() {
		return defaultHandler;
	}

	/**
	 * Sets the default handler for unmatched arguments
	 * @param defaultArg default argument handler
	 */
	public void setDefaultHandler(String defaultArg) {
		CLIArgument<? super ResultType> newDefaultHandler
				= arguments.get(defaultArg);
		if (newDefaultHandler == null) {
			throw new IllegalArgumentException(
					"No such argument: " + defaultArg);
		} else {
			defaultHandler = newDefaultHandler;
		}
	}

	/**
	 * Performs an argument parse on the given string arguments
	 * @param result	Fresh result, that will be filled during the parse
	 * @param args		Iterator pointing to the first argument string
	 * @return			Parse result
	 */
	public ParseResult<ResultType> parse(
			ResultType result, Iterator<String> args) {
		
		Set<?> required = (Set<?>) requiredArguments.clone();
		ParseResult<ResultType> res = new ParseResult<>(result);
		
		while (args.hasNext()) {
			String argString = args.next();
			CLIArgument<? super ResultType> arg = arguments.get(argString);
			if (arg == null) {
				if (defaultHandler == null) {
					res.addError(new ParseError<ResultType>(null,
							Errors.UNRECOGNIZED_ARGUMENT.errorFormat, 
							argString));
				} else {
					arg = defaultHandler;
				}
			} 

			if (arg != null) {
				boolean ok = arg.inserted(argString, args, res);
				required.remove(arg);
				res.addArgument(arg);
				if (ok) {
					continue;
				}
			}
			
			res.setOk(false);
			return res;
		}
		
		if (required.isEmpty()) {
			for (CLIArgument<? super ResultType> arg
					: getArguments().values()) {
				if (!arg.isOk(res)) {
					res.setOk(false);
					return res;
				}
			}
			res.setOk(true);
		} else {
			for (Object o : required) {
				res.addError(new ParseError<ResultType>(null,
						Errors.MISSING_REQUIRED.errorFormat ,
						((CLIArgument<? super ResultType>)o).matcher()));
			}
			res.setOk(false);
		}
		
		return res;
	}
}
